For each iteration of the calibration all the metrics listed in Table below are calculated and a number of plots are generated to ease the assessment of the progress made at a given iteration. These plots help to check whether the calibration is heading toward the right direction and diagnose for possible bugs and disruption in the calibration process. The table below summarized all the metrics that are currently calculated as a part of calibration process.
| Metric | Equation | Optimal Value | Purpose |
|---|---|---|---|
| Objective Function | User specified | 0 | Used in the minimization process of DDS |
| Nash-Sutcliffe Efficiency (NSE) | $ NSE = 1 - \frac{\sum (Q_o - Q_s)^2 } {\sum (Q_o - \bar{Q_o})^2} $ | 1 | Single metric combining timing and magnitude errors |
| Normalized Nash-Sutcliffe Efficiency (NNSE) | $ NNSE = \frac{1}{2-NSE} $ | 1 | Normalized Single metric combining timing and magnitude errors, |
| Normalized Nash-Sutcliffe Efficiency Squared (NNSEsq) | $ NNSEsq = \frac{1}{2-NSE^{Sq}} \\ where: \\ NSE \space streamflow \space inputs \space are \space squared $ | 1 | Normalized NSE where the inputs to the original NSE equation are squared before normalizing. |
| Log-transformed NSE (NSELog) | $ NSELog = 1 - \frac{\sum (\log(Q_o) - \log(Q_s))^2 } {\sum (\log(Q_o) - \log(\bar{Q_o})^2)} $ | 1 | Log-tranformed Nash-Sutcliffe Efficiency |
| Weighted NSE (NSEWt) | $ NSEWt = \frac{NSE + NSELog}{2} $ | 1 | Capture flow timing and magnitude errors jointly via the NSE metric and somewhat reduce the peak flow emphasis of NSE by including the log-transformed metric. |
| Pearson Correlation (Cor) | $ r = \frac{ \sum_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y}) }{%\sqrt{\sum_{i=1}^{n}(x_i-\bar{x})^2}\sqrt{\sum_{i=1}^{n}(y_i-\bar{y})^2}} $ | 1 | Flow timing. |
| Root Mean Squared Error (RMSE) | $ RMSE = \sqrt{\frac{1}{n}\Sigma_{i=1}^{n}{\Big(\frac{d_i -f_i}{\sigma_i}\Big)^2}} $) | 0 | Goodness of Fit between observations and model output |
| Percent Bias (Bias) | $ Bias = \frac{\sum{(sim - obs)}} {\sum{obs}} * 100 $ | 0 | Goodness of Fit between observations and model |
| Kling-Gupta Efficiency (KGE) | $KGE = 1 - \sqrt{ (s[1]*(r-1))^2 +(s[2]*(\alpha-1))^2 + (s[3]*(\beta-1))^2 }$where:$\alpha=\sigma_s/\sigma_o$ $\beta=\mu_s/\mu_o$ | 1 | Single metric combining timing and magnitude errors |
| Multi-Scale Objective Function (MSOF) | $ MSOF = \sqrt{\sum{(\frac{\sigma_1}{\sigma_k})^2}\sum{(q_{o,k,i} -q_{s,k,l}}(X))^2 } $ | 0 | Simultaneously consider contributions from a wide range of time scales of aggregation during the calibration process (i.e., mimicking manual calibration), and reduce the likelihood of the search getting stuck in small ‘pits’, by smoothing the objective function surface |
| hyperResMultiObj | $ hRMO = w_o (1.0 - NNSE) + (w_1 \mid P_e \mid ) + (w_2 \mid V_e \mid) \\ \\ where: \\ NNSE = Normalized NSE,\\ P_e = Peas Discharge Error, \\ V_e = Volume Discharge $ | 0 | Maximize NNSE while minimizing flow volume and peak bias error |
| peak_bias | $ Peak\_bias = \frac{\overline{\mid P_m - P_o \mid}}{P_o}*100) \\ Where: \\ p_m = model peaks for matched events \\ P_o = observation peaks for matched events $ | 0 | Quantifies the overall peak bias in matched events between the model and observation throughout the calibration window. |
| event_volume_bias | $ Event \space Volume \space bias = \overline{ \frac{\mid V_m - V_o) \mid} {V_o}} * 100 \\ where: \\ V_m = volume \space model \\ V_o = volume \space observed $ | 0 | Quantifies the overall volume bias in matched events between the model and observation throughout the calibration window. |
| eventmultiobj | $ W1 * abs(Pbiaspeak) + W2 * abs(VEFlow) $ | 0 | Account for peak error and volume error. |
| peak_tm_err_hr | $ peak\_tm\_err\_hr = \overline{(\mid T_m - T_o \mid)} \\ where: \\ T_m = model \space time \space (hours) \\ T_o = observation \space time \space (hrs)$ | 0 | Quantifies the central value of time offset between observed and modeled peak events in hours |
| Probability of Detection (POD) | $$POD = \frac{a}{a+c}$$ | 1 | Probability that a flood was observed when it was forecasted; where 1 is a perfect score and 0 is the worst. |
| False Alarm Ratio (FAR) | $$ FAR = \frac{b}{a+b}$$ | 0 | Probability that a flood was forecasted, but not observed; where 0 is perfect and 1 is the worst |
| Critical Success Index (CSI) | $$ CSI = \frac{a}{a+b+c}$$ | 0 | The proportion of correctly forecast floods over all floods, either forecast or observed |
| Stedinger’s (1981) lognormal estimator of correlation (corr1) | $ r1 = \frac{e^{\sigma^{2UV}}-1}{\sqrt{e^{\sigma^{2U}}e^{\sigma^{2V}}}} $ | 1 | Stedinger’s (1981) lognormal estimator of correlation (corr1) Purpose: Improved estimator of correlation when hydrologic data is skewed (non-normal), which is often the case for daily and subdaily streamflow https://doi.org/10.1080/02626667.2019.1686639 |
| Lamontagne-Barber Efficiency Mixture Model (lbem) | See Ref. | 1 | Improved theoretical estimator of efficiency based on NSE for when hydrologic data is skewed (non-normal) and exhibits periodicity (e.g., seasonality), which is often the case for daily and subdaily streamflow. |
| Lamontagne-Barber Efficiency Estimators (lbemprime) | See Ref. | 1 | Improved theoretical estimator of efficiency based on KGE for when hydrologic data is skewed (non-normal) and exhibits periodicity (e.g., seasonality), which is often the case for daily and subdaily streamflow. (Lamontagne 2020 https://doi.org/10.1029/2020WR027101) |
In the above table, POD, FAR, and CSI are using the threshold predefined in the obsStrData.Rdata file for the threshold.
After 1st iteration model (WRF-Hydro) run is complete, then a script called calib_workflow will do the followings:
plots will be populated in the RUN.CALIB directory with a number of plots depending on what options have been activated in the setup.parm file. The example in the previous lessons was using only the streamflow observation for calibration and therefore there is only plots related to the progress of parameters as well as streamflow error metrics and hydrographs. Let's take a look at few of these plots.
%%bash
# List all the plots generated while calibration is progressing
ls /home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots
01447720_calib_run_obj_outlier.png 01447720_calib_run_obj.png 01447720_hydrograph.png 01447720_metric_calib_run_outlier.png 01447720_metric_calib_run.png 01447720_obj_vs_parameters_calib_run_outlier.png 01447720_obj_vs_parameters_calib_run.png 01447720_parameters_calib_run_outlier.png 01447720_parameters_calib_run.png 01447720_scatter.png
As seen above some of the plots are duplicated with the tag name outlier. This is added to the calibration workflow to remove the very large numbers (ouliers) from the plots so they are readable. Figures with the tag _outlier are containing all the interations and are the ones that we will check here. Let's begin with checking how the objective function is progressing with iterations.
#Generate plots:
import os
from IPython.display import Image
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/"
_filename = "01447720_calib_run_obj_outlier.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 400, height = 300)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) ==True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/01447720_calib_run_obj_outlier.png>
The above plot keep tracks of how objective function defined in the setup.parm file is progressing with iterations. It should be noted that DDS is a minimization algorithm. If user select an objective function like KGE that the ideal value is the max value, then 1-KGE is used as objective function. The same goes for different variant of NSE, correlation coefficient and lbem. A healthy calibration procedure would asymptote at the higher iterations numbers when we are getting close to the user defined number of iterations. The red star sign shows the iterations which has the best results so far.
Next, we will take a look at how other error metrics are progressing with iterations next.
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/"
_filename = "01447720_metric_calib_run_outlier.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/01447720_metric_calib_run_outlier.png>
The above figure shows how different streamflow metrics are evolving with iterations. Ideally the overall trend is that with the improvement of the objective function, all other metrics improve as well. However, that is not the case always and therefore we keep an eye on the performance of the model to make sure we are not degrading other model performance aspects during calibration.
Plot below is the equivalent plot from official NWMv21 calibration. Note that the objective function in NWMv21 was 1 - weighted NSE and log NSE while the objective function used in this exercise is 1 - KGE. As part of NWMv30 RnD, we tested several different objective functions and decided to use KGE for NWMv30 onboarding, and therefore used in this training. Also note, there is a number of metrics that are reported in NWMv30 that did not exists in NWMv21 calibration. In NWMV21 for this gage with improvements in the objective function,other metrics such correlation coefficient, KGE and etc were also improved. This is the desired outcome.
Let's take a look at how parameters have evolved with calibration progress.
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/"
_filename = "01447720_parameters_calib_run_outlier.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/01447720_parameters_calib_run_outlier.png>
The above plot shows how parameters are changing with iterations, it is one way to make sure you are calibrating all the parameters that you flagged. Note that in the early iterations, a larger subset of parameters are being perturbed and as we reach to the end of calibration (larger iterations), fewer parameters are purturbed. It might be more obvious in plot below which is for the same gage from NWMv21.
In initial iterations, the DDS algorithm searches globally and as the procedure approaches the maximum user-defined number of iterations, the search transitions from a global to a local search. This transition from a global to local search is achieved by dynamically and probabilistically reducing the search dimension which is the subset of the calibration parameters that will be updated in a given iteration.
The probability of a parameter to be chosen for inclusion in the search is equal to P(i) = 1 - ln(i)/ln(m), where i is the iteration number and m is the maximum iteration number. Therefore the possibility of a parameter to be chosen reduces with increase in iteration numbers. In the initial iterations, almost all the parameters will be modified and as it approaches the maximum number of iterations it will only modify a few parameters or only one. Parameters selected in each iteration are perturbed within the defined parameter range. The suggested lower and upper limits were shown in lesson 1. The limits are selected based on previous literature review and experts opinion.
The maximum number of iterations used in the previous versions of NWM calibration was set to 300 except for the domains that were too large (> 5000 km2) in that case 150 iterations were used for the calibration.
Next, we will take a look at streamflow hydrograph and the scatter plots.
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/"
_filename = "01447720_hydrograph.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/01447720_hydrograph.png>
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/"
_filename = "01447720_scatter.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.CALIB/plots/01447720_scatter.png>
This plot has only the model simulations with the default parameters (first iteration), the best parameters and the last iterations. We do not plot all iterations in order to have a cleaner picture. However, all the time series are saved in a Rdataset (called proj_data.Rdata) and could be used afterward to plot any other iterations if required. Below plots are the equivalent plots from NWMv21 for the same gage.
After calibration is finished, the model runs for both the default and best parameters (from calibration step) for the full duration of (calibration/validation). Note, the validation period does not have to be right after calibration, and in fact validation period could be an earlier time than calibration period. Full Period is the extent that has both calibration and validation period in it. If there is a gap between the two period, the full period will have that gap in it also.
Error metrics are calculated for calibration, validation and full period. Finally a set of diagnostic plots are generated depending on what options user has selected in the setup.parm file. We have only calibrated using streamflow observations in the example provided in lesson 2 and 3, and therefore only streamflow plots are provided here. Let us take a look at the metrics first.
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/"
_filename = "01447720_valid_metrics.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/01447720_valid_metrics.png>
This plot summarizes how model performance has changed from default to best calibration iterations as well as how they perform during an independent time period (validation). Usually the performance of the validation period is not as good as the calibration period.
The main task after calibration and validation workflow finishes is the classification of the calibration basin to Donor, Keep and Drop basins that will be used then in regionalization. Definition of Donor, Keep and Drop basins is as follows:
Below is the same plot from NWMv21. As one can see all the metrics improved compared to the default for both the calibration and validation period.
Let's also look at the hydrograph and scatter plots for our current experiment.
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/"
_filename = "01447720_valid_hydrogr.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/01447720_valid_hydrogr.png>
#Generate plots:
_gen_file = ""
dir_path = "/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/"
_filename = "01447720_valid_scatter.png"
# Load image from local storage
if os.path.isfile(dir_path + _filename):
_gen_file = (dir_path + _filename)
i = Image(filename = _gen_file, width = 600, height = 400)
else:
print('file: <' + _filename + '> not generated yet')
# display plot (if. available)
if os.path.isfile(dir_path + _filename) == True:
display(i)
print( "file path: <" + dir_path + _filename + ">" )
file path: </home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/plots/01447720_valid_scatter.png>
Here is the equivalent plots from NWMv21.
All the paramters and error metrics for each iteraion of calibration and validation are stored in the data base and could be accessed using the sqlite library in python. Since our example case has only one gage, it is very easy to understand the content of the DB. Let's take a look at all the tables that exists in the DB and their content. Let's begin with the Domain_Meta table.
import sqlite3
import pandas as pd
# Let s read the Database and print the name of all the available tables in the database
dat = sqlite3.connect("/home/docker/example_case/Calibration/output/DATABASE.db")
cursor = dat.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
names = cursor.fetchall()
names
[('Calib_Params',),
('Sens_Params',),
('Calib_Stats',),
('Sens_Stats',),
('Domain_Meta',),
('Job_Meta',),
('Job_Params',),
('Valid_Stats',)]
# Lets print the content of the Domain_Meta table in the DB
_name = 'Domain_Meta'
query = dat.execute("SELECT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols).transpose()
print(_name)
results
Domain_Meta
| 0 | |
|---|---|
| domainID | 1 |
| gage_id | 01447720 |
| link_id | 4185779 |
| domain_path | /home/docker/example_case/Calibration/Input_Fi... |
| gage_agency | USGS |
| geo_e | 4039 |
| geo_w | 4018 |
| geo_s | 2264 |
| geo_n | 2291 |
| hyd_e | 16156 |
| hyd_w | 16069 |
| hyd_s | 9053 |
| hyd_n | 9164 |
| geo_file | /home/docker/example_case/Calibration/Input_Fi... |
| land_spatial_meta_file | /home/docker/example_case/Calibration/Input_Fi... |
| wrfinput_file | /home/docker/example_case/Calibration/Input_Fi... |
| soil_file | /home/docker/example_case/Calibration/Input_Fi... |
| fulldom_file | /home/docker/example_case/Calibration/Input_Fi... |
| rtlink_file | /home/docker/example_case/Calibration/Input_Fi... |
| spweight_file | /home/docker/example_case/Calibration/Input_Fi... |
| gw_file | /home/docker/example_case/Calibration/Input_Fi... |
| gw_mask | -9999 |
| lake_file | /home/docker/example_case/Calibration/Input_Fi... |
| forcing_dir | /home/docker/example_case/Calibration/Input_Fi... |
| obs_file | /home/docker/example_case/Calibration/Input_Fi... |
| site_name | Tobyhanna Creek near Blakeslee, PA |
| lat | 41.086948 |
| lon | -75.605896 |
| area_sqmi | 118.914474 |
| area_sqkm | 307.9872 |
| county_cd | 025 |
| state | nan |
| huc2 | nan |
| huc4 | nan |
| huc6 | nan |
| huc8 | nan |
| ecol3 | nan |
| ecol4 | nan |
| rfc | MARFC |
| dx_hydro | 250.0 |
| agg_factor | 4 |
| hydro_tbl_spatial | /home/docker/example_case/Calibration/Input_Fi... |
| opt_spin_land_path | -9999 |
| opt_spin_hydro_path | -9999 |
| chan_parm_path | -9999 |
We provided definitions of all the inputs in Lesson 2, refer to the provided table for descriptions. gage_id, link_id and domain_path were the only required inputs from the user, and the rest of the information can be left empty and some are filled out by the python workflow. Our example domain is the contributing area to USGS gage 01447720 and the ComID of link (where gage is located on) is 4185779. The link_id or ComID of reach is identifier used to collect streamflow data from the model outputs. domainID is the identifier that is used to connect this table to other tables in the database. There is a unique domainID for each gage. domaiID is assinged by the python workflow and not the user.
Now let us take a look at the Job_Meta table.
# Lets print the content of the Job_Meta table in the DB
_name = 'Job_Meta'
query = dat.execute("SELECT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols).transpose()
print(_name)
results
Job_Meta
| 0 | |
|---|---|
| jobID | 1 |
| Job_Directory | /home/docker/example_case/Calibration/output//... |
| date_su_start | 2010-10-01 00:00:00 |
| date_su_end | 2011-01-01 00:00:00 |
| su_complete | 1 |
| sens_flag | 0 |
| sens_table | /home/docker/PyWrfHydroCalib/setup_files/sens_... |
| num_sens_sample | 3 |
| num_sens_iter | 0 |
| sens_batch | 1 |
| date_sens_start | 2013-07-01 00:00:00 |
| date_sens_end | 2013-08-01 00:00:00 |
| date_sens_start_eval | 2013-07-05 00:00:00 |
| sens_complete | 0 |
| calib_flag | 1 |
| calib_table | /home/docker/PyWrfHydroCalib/setup_files/calib... |
| date_calib_start | 2011-01-01 00:00:00 |
| date_calib_end | 2011-06-01 00:00:00 |
| date_calib_start_eval | 2011-02-01 00:00:00 |
| num_iter | 10 |
| calib_complete | 1 |
| valid_start_date | 2011-01-01 00:00:00 |
| valid_end_date | 2012-01-01 00:00:00 |
| valid_start_date_eval | 2011-06-01 00:00:00 |
| valid_complete | 0 |
| acct_key | |
| que_name | |
| num_cores_model | 2 |
| num_nodes_model | 1 |
| num_cores_per_node | 2 |
| job_run_type | 4 |
| exe | /home/docker/wrf_hydro_nwm_public/trunk/NDHMS/... |
| num_gages | 1 |
| owner | docker |
| MISSING | |
| slack_channel | MISSING |
| slack_token | MISSING |
| slack_user | MISSING |
| mpi_cmd | mpiexec -np |
| cpu_pin_cmd |
The JobID is specified by the user at the time of submission of the calibration and bundles all the basins that have the same calibration properties. The rest of the information in this table comes from the setup.parm file that is filled by user (refer to Lesson 1). For description of all the columns refer to Lesson 2.
Now let ue take a look at the Job_PARAM table.
# Lets print the content of the Job_Params table in the DB
_name = 'Job_Params'
query = dat.execute("SELECT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols).transpose()
print(_name)
results
Job_Params
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| jobID | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| param | bexp | smcmax | dksat | refkdt | slope | retdeprtfac | lksatfac | zmax | expon | cwpvt | vcmx25 | mp | mfsno | rsurfexp |
| defaultValue | 1.0 | 1.0 | 1.0 | 1.0 | 0.3 | 1.0 | 1000.0 | 50.0 | 3.0 | 1.0 | 1.0 | 1.0 | 1.0 | 5.0 |
| min | 0.4 | 0.8 | 0.2 | 0.1 | 0.0 | 0.1 | 10.0 | 10.0 | 1.0 | 0.5 | 0.6 | 0.6 | 0.25 | 1.0 |
| max | 1.9 | 1.2 | 10.0 | 4.0 | 1.0 | 20000.0 | 10000.0 | 250.0 | 8.0 | 2.0 | 1.4 | 1.4 | 2.0 | 6.0 |
| sens_flag | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| calib_flag | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
The above table lists all the parameters used for calibrations with the range and default value to start from. All the basins in a given job will use the same default and range for their parameters. The table was prepared and explained in Lesson 1. Refer to Lesson 1 for definition of these parameters. Next, take a look at the Calib_Stats table which has the calibration related information.
# Lets print the content of the Calib_Stats table in the DB
_name = 'Calib_Stats'
query = dat.execute("SELECT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols).transpose()
print(_name)
df_calib_stats = results
results
Calib_Stats
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|---|
| jobID | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
| domainID | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
| iteration | 1.000000 | 2.000000 | 3.000000 | 4.000000 | 5.000000 | 6.000000 | 7.000000 | 8.000000 | 9.000000 | 10.000000 |
| objfnVal | 0.674176 | 0.490465 | 0.437498 | 0.472649 | 0.460179 | 0.485098 | 0.436791 | 0.436791 | 0.436791 | 0.436791 |
| bias | -29.775313 | -27.901886 | -27.783369 | -28.574206 | -28.086634 | -28.476965 | -27.714163 | -27.714163 | -27.714163 | -27.714163 |
| rmse | 19.338323 | 14.045320 | 12.906909 | 13.823071 | 13.477427 | 14.093751 | 12.914141 | 12.914141 | 12.914141 | 12.914141 |
| cor | 0.818773 | 0.933083 | 0.936392 | 0.927632 | 0.931787 | 0.926663 | 0.935844 | 0.935844 | 0.935844 | 0.935844 |
| nse | 0.463204 | 0.716838 | 0.760880 | 0.725728 | 0.739273 | 0.714882 | 0.760612 | 0.760612 | 0.760612 | 0.760612 |
| nselog | 0.415884 | 0.633356 | 0.564484 | 0.576734 | 0.586997 | 0.590384 | 0.560276 | 0.560276 | 0.560276 | 0.560276 |
| kge | 0.325824 | 0.509535 | 0.562502 | 0.527351 | 0.539821 | 0.514902 | 0.563209 | 0.563209 | 0.563209 | 0.563209 |
| fdcerr | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| msof | 999.061243 | 725.589880 | 667.038627 | 714.280597 | 696.434477 | 728.239777 | 667.410067 | 667.410067 | 667.410067 | 667.410067 |
| hyperResMultiObj | 0.390884 | 0.311573 | 0.285119 | 0.306259 | 0.297982 | 0.311148 | 0.285065 | 0.285065 | 0.285065 | 0.285065 |
| nnsesq | 0.551879 | 0.628031 | 0.659439 | 0.635305 | 0.643709 | 0.631114 | 0.659285 | 0.659285 | 0.659285 | 0.659285 |
| eventmultiobj | 36.514274 | 38.763663 | 27.760170 | 31.456200 | 30.727599 | 39.269422 | 31.264655 | 31.264655 | 31.264655 | 31.264655 |
| lbem | 0.316813 | 0.695768 | 0.478167 | 0.539658 | 0.539042 | 0.688832 | 0.470845 | 0.470845 | 0.470845 | 0.470845 |
| lbemprime | 0.370448 | 0.769284 | 0.635498 | 0.674585 | 0.673408 | 0.760166 | 0.632230 | 0.632230 | 0.632230 | 0.632230 |
| corr1 | 0.700011 | 0.880498 | 0.873484 | 0.877536 | 0.875564 | 0.868615 | 0.873280 | 0.873280 | 0.873280 | 0.873280 |
| pod | 0.438424 | 0.800493 | 0.775862 | 0.780788 | 0.785714 | 0.748768 | 0.780788 | 0.780788 | 0.780788 | 0.780788 |
| far | 0.330827 | 0.024024 | 0.003165 | 0.003145 | 0.003125 | 0.012987 | 0.003145 | 0.003145 | 0.003145 | 0.003145 |
| csi | 0.360324 | 0.785024 | 0.773956 | 0.778870 | 0.783784 | 0.741463 | 0.778870 | 0.778870 | 0.778870 | 0.778870 |
| nnse | 0.650704 | 0.779325 | 0.807024 | 0.784762 | 0.793193 | 0.778138 | 0.806850 | 0.806850 | 0.806850 | 0.806850 |
| peak_bias | 45.464006 | 52.537911 | 41.812478 | 47.982152 | 46.473411 | 51.104437 | 41.828330 | 41.828330 | 41.828330 | 41.828330 |
| peak_tm_err_hr | 12.333333 | 3.000000 | 6.000000 | 8.000000 | 7.500000 | 7.000000 | 6.000000 | 6.000000 | 6.000000 | 6.000000 |
| event_volume_bias | 23.089677 | 18.102290 | 6.681709 | 6.667271 | 7.108881 | 21.516901 | 15.419142 | 15.419142 | 15.419142 | 15.419142 |
| cor_snow | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| rmse_snow | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| bias_snow | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| nse_snow | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| kge_snow | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| cor_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| rmse_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| bias_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| nse_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| kge_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| kge_alpha_soil | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 | -9999.000000 |
| best | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 |
| complete | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
The table above sumamrizes error metrics for each gage (specified with the domianID) at every iterations during calibration. Parameters for each iterations are stored in a different table called Calib_Params, let us check it out.
# Lets print the content of the Calib_Params table in the DB
_name = 'Calib_Params'
query = dat.execute("SELECT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols)
print(_name)
df_calib_params = results
df_calib_params
Calib_Params
| jobID | domainID | iteration | paramName | paramValue | |
|---|---|---|---|---|---|
| 0 | 1 | 1 | 1 | bexp | 1.000000 |
| 1 | 1 | 1 | 1 | smcmax | 1.000000 |
| 2 | 1 | 1 | 1 | dksat | 1.000000 |
| 3 | 1 | 1 | 1 | refkdt | 1.000000 |
| 4 | 1 | 1 | 1 | slope | 0.300000 |
| ... | ... | ... | ... | ... | ... |
| 135 | 1 | 1 | 10 | cwpvt | 1.143170 |
| 136 | 1 | 1 | 10 | vcmx25 | 1.150821 |
| 137 | 1 | 1 | 10 | mp | 0.981892 |
| 138 | 1 | 1 | 10 | mfsno | 1.112488 |
| 139 | 1 | 1 | 10 | rsurfexp | 3.526781 |
140 rows × 5 columns
The User can define which iterations are best performing from the Calib_Stats and find the correponding parameter values in Calib_Params table.
# investigate the best performing parameter
#turn on/off displayed table row/column lengths:
pd.options.display.max_columns = 10
pd.options.display.max_rows = 50
# select the domainID
_domainID = 1
# Identify Iteration with the best performance (minimum objective function)
df_calib_stats_T = df_calib_stats.T
subset = df_calib_stats_T[df_calib_stats_T.domainID == _domainID]
bestIter = subset['objfnVal'].astype(float).idxmin() + 1
# ^ notes: seeking minimum Objective Function Value across all iterations, where index number +1 = iteration number due to zero vs one indexing
print ('the best iteration is: ' + str(bestIter) )
the best iteration is: 7
# select all parameters from the best iteration:
df_calib_params.loc[df_calib_params['iteration'] == int(bestIter)]
| jobID | domainID | iteration | paramName | paramValue | |
|---|---|---|---|---|---|
| 84 | 1 | 1 | 7 | bexp | 0.566741 |
| 85 | 1 | 1 | 7 | smcmax | 0.994643 |
| 86 | 1 | 1 | 7 | dksat | 1.973026 |
| 87 | 1 | 1 | 7 | refkdt | 0.790521 |
| 88 | 1 | 1 | 7 | slope | 0.651748 |
| 89 | 1 | 1 | 7 | retdeprtfac | 6329.988225 |
| 90 | 1 | 1 | 7 | lksatfac | 5778.335423 |
| 91 | 1 | 1 | 7 | zmax | 23.239516 |
| 92 | 1 | 1 | 7 | expon | 2.735032 |
| 93 | 1 | 1 | 7 | cwpvt | 1.619775 |
| 94 | 1 | 1 | 7 | vcmx25 | 1.150821 |
| 95 | 1 | 1 | 7 | mp | 0.981892 |
| 96 | 1 | 1 | 7 | mfsno | 1.112488 |
| 97 | 1 | 1 | 7 | rsurfexp | 3.526781 |
Finally the last relevant table is Valid_Stats which summarizes the error metrics at the calibration, validation and full period for model runs with the default and best parameters.
# Lets print the content of the Valid_Stats table in the DB
_name = 'Valid_Stats'
query = dat.execute("SELECT DISTINCT * From" + " " + str(_name))
cols = [column[0] for column in query.description]
results= pd.DataFrame.from_records(data = query.fetchall(), columns = cols).transpose()
print(_name)
results
Valid_Stats
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| jobID | 1 | 1 | 1 | 1 | 1 | 1 |
| domainID | 1 | 1 | 1 | 1 | 1 | 1 |
| simulation | default | default | default | calibrated | calibrated | calibrated |
| evalPeriod | calib | valid | full | calib | valid | full |
| objfnVal | 0.674201 | 0.573236 | 0.618966 | 0.436784 | 0.218694 | 0.339959 |
| bias | -29.779096 | -16.366809 | -22.438376 | -27.711403 | -17.255228 | -21.988602 |
| rmse | 19.34208 | 10.316586 | 14.027632 | 12.916404 | 6.563171 | 9.211039 |
| cor | 0.818762 | 0.654122 | 0.75342 | 0.935841 | 0.885376 | 0.904595 |
| nse | 0.463173 | 0.398529 | 0.461668 | 0.760607 | 0.756572 | 0.767888 |
| nselog | 0.41581 | 0.584694 | 0.57727 | 0.560461 | 0.561589 | 0.601178 |
| nseWt | 0.439491 | 0.491612 | 0.519469 | 0.660534 | 0.659081 | 0.684533 |
| kge | 0.325799 | 0.426764 | 0.381034 | 0.563216 | 0.781306 | 0.660041 |
| msof | 998.911697 | 746.461991 | 1247.042519 | 667.281018 | 474.048186 | 818.680534 |
| hyperResMultiObj | 0.390904 | 0.341891 | 0.361796 | 0.285056 | 0.214096 | 0.260257 |
| nnsesq | 0.551876 | 0.522255 | 0.551408 | 0.659282 | 0.641673 | 0.659871 |
| eventmultiobj | 36.514274 | 24.221904 | 32.388691 | 31.264655 | 31.104932 | 31.133083 |
| lbem | 0.316813 | 0.001993 | 0.215444 | 0.470845 | -1.278021 | -0.233074 |
| lbemprime | 0.370448 | 0.215299 | 0.333015 | 0.63223 | 0.122061 | 0.408691 |
| corr1 | 0.699989 | 0.680584 | 0.688863 | 0.873273 | 0.730383 | 0.781077 |
| pod | 0.438424 | 0.561644 | 0.507088 | 0.780788 | 0.796477 | 0.789531 |
| far | 0.330827 | 0.213699 | 0.263074 | 0.003145 | 0.159091 | 0.097257 |
| csi | 0.360324 | 0.487267 | 0.429363 | 0.77887 | 0.692177 | 0.727638 |
| nnse | 0.650691 | 0.624426 | 0.650055 | 0.806847 | 0.804228 | 0.811614 |
| peak_bias | 45.464006 | 15.350966 | 32.25313 | 41.82833 | 26.003516 | 32.986833 |
| peak_tm_err_hr | 12.333333 | 23.666667 | 20.8 | 6.0 | 37.0 | 8.0 |
| event_volume_bias | 23.089677 | 37.528311 | 32.592033 | 15.419142 | 38.757055 | 28.352459 |
| obj_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| cor_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| rmse_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| bias_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| nse_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| kge_snow | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| obj_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| cor_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| rmse_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| bias_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| nse_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| kge_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
| kge_alpha_soil | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 | -9999.0 |
Simulations states whether the model run is with the default or calibrated (best) parameters. evalPeriod indicate what the period of evaluations is. The options are calib which is the calibration period, valid is validation period and full is the period (continuous) that has both calibration and validation in it. Note the claibration and validation period do not have to be following each other, there could be a gap between the two or validation could happen at the years prior to calibration.
Here we will spatially visualize the difference in a parameter field between the default CTRL output and the BEST selected optimum parameter.
#initialize libraries and template
%matplotlib inline
import matplotlib.pyplot as plt
import xarray as xr
xr.set_options(display_style="html")
<xarray.core.options.set_options at 0x7f85eb223ac0>
Let us first take a look at how values for LKSATFAC in the fine resolution file (Fuldom.nc) file has changed during our experiment.
# Looking at RUN.VALID OUTPUTS:
file_CTRL ='/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/OUTPUT/CTRL/Fulldom.nc'
fulldom_CTRL = xr.open_dataset(file_CTRL)
#fulldom_CTRL.LKSATFAC.plot()
file_BEST = '/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/OUTPUT/BEST/Fulldom.nc'
fulldom_BEST = xr.open_dataset(file_BEST)
#fulldom_BEST.LKSATFAC.plot()
#CONFIGURE PLOTS:
fig, axes = plt.subplots(ncols=2,figsize=(15, 7))
#left panel plot
fulldom_CTRL.LKSATFAC.plot(ax = axes[0])
axes[0].set_title('LKSATFAC CTRL')
#right panel plot
fulldom_BEST.LKSATFAC.plot(ax = axes[1])
axes[1].set_title('LKSATFAC BEST')
/home/docker/miniconda3/lib/python3.9/site-packages/xarray/conventions.py:512: SerializationWarning: variable 'TOPOGRAPHY' has multiple fill values {-3.4028235e+38, -9999.0}, decoding all values to NaN.
new_vars[k] = decode_cf_variable(
Text(0.5, 1.0, 'LKSATFAC BEST')
As shown above LKSATFAC starts from the uniform value of 1000, this is the default value specified in the calib.parm file. Since this parameter is uniform in space, the parameter is a substitute and not a multiplier, therefore the best LKSATFAC would be a uniform value as well which is equal to what you got from the calibration process.
Next we will take a look at the parameter SMCMAX which exists in both soil_properties.nc file and also HYDRO_TBL_2D.nv file. SMCMAX is a function of soil type and therefore it is not uniform in space (check the CTRL run) and is being formulated as a multiplier in the calibration procedure.
# Soil Properties difference:
path = '/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/OUTPUT/'
# Load CTRL Soil Props:
soil_ctrl = 'CTRL/soil_properties.nc'
soil_ctrl = xr.open_dataset(path + soil_ctrl)
# load BEST Soil Props:
soil_best = 'BEST/soil_properties.nc'
soil_best = xr.open_dataset(path + soil_best)
#compute the SMCMAX multiplier:
soil_multiplier = soil_best.smcmax / soil_ctrl.smcmax
#CONFIGURE PLOTS:
fig, axes = plt.subplots(ncols=3,figsize=(15, 5))
#left panel plot
soil_ctrl.smcmax.sel(soil_layers_stag = 0).plot(ax = axes[0])
axes[0].set_title('SMCMAX CTRL')
#middle panel plot
soil_best.smcmax.sel(soil_layers_stag = 0).plot(ax = axes[1])
axes[1].set_title('SMCMAX BEST')
#right panel plot
soil_multiplier.sel(soil_layers_stag = 0).plot(ax = axes[2])
axes[2].set_title('SMCMAX Multiplier')
Text(0.5, 1.0, 'SMCMAX Multiplier')
As shown above, the CTRL SMCMAX and BEST SMCMAX are both spatially varying, and the spatial pattern of the CTRL is preserved in the BEST since we used a multiplier. Multiplier value in third figure is the same as the value in the best parameter table above. This parameter is used by both LSM and hydro part of the model and therefore exists in the Hydro 2d files also. Let's confirm they match betweent the two files (they should).
# Hydro 2D CTRL:
file_2D_ctrl = '/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/OUTPUT/CTRL/HYDRO_TBL_2D.nc'
hydro2d_ctrl = xr.open_dataset(file_2D_ctrl)
# Hydro 2D BEST
file_2D_best = '/home/docker/example_case/Calibration/output/example1/01447720/RUN.VALID/OUTPUT/BEST/HYDRO_TBL_2D.nc'
hydro2d_best = xr.open_dataset(file_2D_best)
# Generate and Plot Difference: SMCMAX1
SMCMAX1_multiplier = hydro2d_best.SMCMAX1 / hydro2d_ctrl.SMCMAX1
# SMCMAX1_multiplier.plot(vmin=-0.1,vmax=0.1,cmap="RdBu")
#CONFIGURE PLOTS:
fig, axes = plt.subplots(ncols=3,figsize=(15, 5))
#left panel plot
hydro2d_ctrl.SMCMAX1.plot(ax = axes[0])
axes[0].set_title('SMCMAX1 CTRL')
#middle panel plot
hydro2d_best.SMCMAX1.plot(ax = axes[1])
axes[1].set_title('SMCMAX1 BEST')
#right panel plot
SMCMAX1_multiplier.plot(ax = axes[2])
axes[2].set_title('SMCMAX1 Multiplier')
Text(0.5, 1.0, 'SMCMAX1 Multiplier')
We reviewed the plots that are generated during calibration and after validation and described the type of the information they provide. Database content also was investigated. You can now move to the next lesson that describes some of the errors that you might run into while trying to use PyWrfHydroCalib tool.